import json
import os
import re
from typing import Any, Dict, List, Optional

import requests
import traceback
import time

from .config import Config
from .logger import log_event


class DoubaoLLM:
    def __init__(self, api_key: Optional[str] = None, api_url: Optional[str] = None, model: Optional[str] = None):
        self.api_key = api_key or Config.DOABAO_API_KEY if hasattr(Config, 'DOABAO_API_KEY') else (api_key or Config.DOUBAO_API_KEY)
        self.api_url = api_url or Config.DOUBAO_API_URL
        self.model = model or Config.DOUBAO_MODEL
        self.last_parse_mode = "rule"
        self.last_generate_mode = "template"
    
    def available(self) -> bool:
        return bool(self.api_key and self.api_url)

    def chat(self, messages: List[Dict[str, str]], response_json: bool = False, timeout: float = 60) -> str:
        """Send messages to Doubao-like chat completions API and return content.

        This expects an OpenAI-compatible format. Configure api_url/model accordingly.
        """
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }
        payload: Dict[str, Any] = {
            "model": self.model,
            "messages": messages,
            "temperature": 0.2,
        }
        if response_json:
            # Some OpenAI-compatible APIs support structured JSON output
            payload["response_format"] = {"type": "json_object"}

        resp = requests.post(self.api_url, headers=headers, json=payload, timeout=timeout)
        resp.raise_for_status()
        data = resp.json()
        # Try to read OpenAI-style response
        content = (
            data.get("choices", [{}])[0]
            .get("message", {})
            .get("content", "")
        )
        if not content and isinstance(data, dict):
            # Fallback: some APIs return `output_text`
            content = data.get("output_text", "")
        return content

    def infer_alias(self, name: str) -> Optional[str]:
        """Infer canonical app alias or executable name from a user-provided name.

        Returns best-effort guess like "WeChat.exe" or canonical alias key.
        """
        name = (name or "").strip()
        if not name:
            return None
        # Prefer LLM when available
        if self.available():
            prompt = (
                "你是一个应用别名推断器。给出输入的应用中文名称，返回在 Windows 上对应的启动程序文件名或规范英文名。\n"
                "只输出一个字符串，不要解释。\n"
                "例如：\n"
                "- 微信 -> WeChat.exe\n"
                "- 企业微信 -> WeCom.exe\n"
                "- 记事本 -> notepad.exe\n"
                "- 计算器 -> calc.exe\n"
                f"输入：{name}"
            )
            try:
                log_event("LLM_ALIAS_TRY", f"name={name}")
            except Exception:
                pass
            try:
                content = self.chat(
                    messages=[
                        {"role": "system", "content": "你是严谨的中文助手，只输出一个候选名称。"},
                        {"role": "user", "content": prompt},
                    ],
                    response_json=False,
                    timeout=20,
                )
                guess = (content or "").strip().split()[0]
                # 规整：移除多余标点和引号
                guess = re.sub(r"[\"'《》]", "", guess)
                if guess:
                    try:
                        log_event("LLM_ALIAS_RESPONSE", f"name={name}, guess={guess}")
                    except Exception:
                        pass
                    return guess
            except Exception as e:
                try:
                    log_event("LLM_ALIAS_FALLBACK", f"detail={str(e)}")
                except Exception:
                    pass
        # Fallback: heuristic based on known aliases
        try:
            from difflib import get_close_matches
            from .config import Config
            candidates = list(Config.APP_ALIASES.keys()) + list(Config.APP_ALIASES.values())
            matches = get_close_matches(name, candidates, n=1, cutoff=0.6)
            if matches:
                return matches[0]
        except Exception:
            pass
        return None

    # 新增：从自由文本中提取首个有效 JSON 对象（用于不支持 response_format 的模型返回）
    def _extract_json_text(self, text: str) -> Optional[str]:
        if not isinstance(text, str):
            return None
        s = text.strip()
        # 去掉代码块围栏
        try:
            s = re.sub(r"^```(?:json)?\s*|\s*```$", "", s, flags=re.IGNORECASE | re.MULTILINE)
        except Exception:
            pass
        # 直接尝试整体解析
        try:
            obj = json.loads(s)
            if isinstance(obj, dict):
                return s
        except Exception:
            pass
        # 扫描匹配首个平衡的 JSON 对象
        start = s.find('{')
        if start == -1:
            return None
        stack = 0
        in_string = False
        escape = False
        for i in range(start, len(s)):
            ch = s[i]
            if in_string:
                if escape:
                    escape = False
                elif ch == '\\':
                    escape = True
                elif ch == '"':
                    in_string = False
            else:
                if ch == '"':
                    in_string = True
                elif ch == '{':
                    stack += 1
                elif ch == '}':
                    stack -= 1
                    if stack == 0:
                        candidate = s[start:i+1]
                        try:
                            obj = json.loads(candidate)
                            if isinstance(obj, dict):
                                return candidate
                        except Exception:
                            return None
        return None

    def parse_command(self, text: str) -> List[Dict[str, Any]]:
        """Parse input text into a list of executable actions.

        Uses Doubao first; if unavailable or fails, falls back to rule-based parsing.
        """
        if self.available():
            prompt = (
                "你是一个指令解析器。请将用户意图解析成可执行的动作列表。只输出JSON，不要解释。\n"
                "动作类型仅限：\n"
                "- play_music: {\"song_name\": string}\n"
                "- write_article: {\"topic\": string, \"words\": number, \"save_path\": string}\n"
                "- file_op: {\"op\": string, \"src\": string, \"dest\": string, \"name\": string}\n"
                "- app_control: {\"op\": string, \"app\": string}\n"
                "- system_control: {\"op\": string, \"value\": number}\n"
                "- web_search: {\"query\": string}\n"
                "- navigate: {\"url\": string}\n"
                "- browser_click: {\"target\": \"first_result\" | \"link_text\", \"value\": string}\n"
                "- wechat_message: {\"contact\": string, \"text\": string}\n"
                "- qq_message: {\"contact\": string, \"text\": string}\n\n"
                "若用户给出模糊指令，请给出一个needs_clarification动作，示例：\n"
                "- needs_clarification: {\"question\": string}\n\n"
                "JSON示例：{\"actions\":[{\"type\":\"web_search\",\"query\":\"周星驰\"},{\"type\":\"browser_click\",\"target\":\"first_result\"}]}\n\n"
                f"用户指令：{text}"
            )
            # 记录一次LLM解析尝试
            try:
                log_event("LLM_PARSE_TRY", f"url={self.api_url}")
            except Exception:
                pass
            # 1) 调用LLM接口
            try:
                content = self.chat(
                    messages=[
                        {"role": "system", "content": "你是严谨的中文助手，只输出有效JSON。"},
                        {"role": "user", "content": prompt},
                    ],
                    response_json=True,
                )
                
                # 记录响应内容片段用于调试
                try:
                    content_snip = (content[:300] if isinstance(content, str) else str(type(content)))
                    log_event("LLM_PARSE_RESPONSE", f"content_snip={content_snip}")
                except Exception:
                    pass
            except requests.exceptions.HTTPError as he:
                status = getattr(getattr(he, "response", None), "status_code", None)
                body_snip = ""
                try:
                    body_snip = (getattr(getattr(he, "response", None), "text", "") or "")[:300]
                except Exception:
                    body_snip = ""
                try:
                    log_event("LLM_PARSE_FALLBACK", f"reason=HTTPError, status={status}, body_snip={body_snip}")
                except Exception:
                    pass
                # 新增：若 400 且疑似 response_format 不支持，自动重试不带 response_format
                if status == 400:
                    try:
                        log_event("LLM_PARSE_RETRY", "reason=DROP_RESPONSE_FORMAT,status=400")
                    except Exception:
                        pass
                    try:
                        content2 = self.chat(
                            messages=[
                                {"role": "system", "content": "你是严谨的中文助手，只输出有效JSON。"},
                                {"role": "user", "content": prompt},
                            ],
                            response_json=False,
                        )
                    except requests.exceptions.RequestException as rexc2:
                        try:
                            log_event("LLM_PARSE_FALLBACK", f"reason=RequestException_RETRY, detail={str(rexc2)}")
                        except Exception:
                            pass
                    except Exception as e2:
                        try:
                            log_event("LLM_PARSE_FALLBACK", f"reason=LLM_CHAT_EXCEPTION_RETRY, detail={str(e2)}, stack={traceback.format_exc()[:600]}")
                        except Exception:
                            pass
                    else:
                        # 解析第二次返回
                        parsed2 = None
                        try:
                            parsed2 = json.loads(content2)
                        except Exception:
                            # 尝试从自由文本提取 JSON
                            candidate = self._extract_json_text(content2)
                            if candidate:
                                try:
                                    parsed2 = json.loads(candidate)
                                except Exception as je2:
                                    try:
                                        log_event("LLM_PARSE_FALLBACK", f"reason=JSON_DECODE_ERROR_RETRY_2, error={str(je2)}, content_snip={candidate[:200]}")
                                    except Exception:
                                        pass
                            else:
                                try:
                                    snip = content2[:200] if isinstance(content2, str) else str(type(content2))
                                    log_event("LLM_PARSE_FALLBACK", f"reason=JSON_DECODE_ERROR_RETRY, content_snip={snip}")
                                except Exception:
                                    pass
                        if isinstance(parsed2, dict):
                            actions2 = parsed2.get("actions", [])
                            if isinstance(actions2, list) and actions2:
                                self.last_parse_mode = "llm"
                                try:
                                    log_event("LLM_PARSE_MODE", self.last_parse_mode)
                                except Exception:
                                    pass
                                return actions2
                            else:
                                try:
                                    detail = f"type={type(actions2).__name__}, len={len(actions2) if isinstance(actions2, list) else 'N/A'}"
                                    log_event("LLM_PARSE_FALLBACK", f"reason=INVALID_OR_EMPTY_ACTIONS_RETRY, {detail}")
                                except Exception:
                                    pass
                # 新增：对 500/503 或包含 ModelLoading 的情况进行指数退避重试
                if (status == 500 or status == 503) or ("ModelLoading" in body_snip):
                    delays = [0.5, 1.0, 2.0]
                    timeouts = [8, 12, 16]
                    for idx, delay in enumerate(delays, start=1):
                        try:
                            log_event("LLM_PARSE_RETRY", f"reason=HTTP_{status}_BACKOFF, attempt={idx}, delay={delay}s")
                        except Exception:
                            pass
                        try:
                            content_retry = self.chat(
                                messages=[
                                    {"role": "system", "content": "你是严谨的中文助手，只输出有效JSON。"},
                                    {"role": "user", "content": prompt},
                                ],
                                response_json=True,
                                timeout=timeouts[idx-1],
                            )
                        except Exception as e_retry:
                            try:
                                log_event("LLM_PARSE_FALLBACK", f"reason=RETRY_EXCEPTION, attempt={idx}, detail={str(e_retry)}")
                            except Exception:
                                pass
                            time.sleep(delay)
                            continue
                        else:
                            # 解析重试返回
                            parsed_retry = None
                            try:
                                parsed_retry = json.loads(content_retry)
                            except Exception:
                                candidate = self._extract_json_text(content_retry)
                                if candidate:
                                    try:
                                        parsed_retry = json.loads(candidate)
                                    except Exception as je_r2:
                                        try:
                                            log_event("LLM_PARSE_FALLBACK", f"reason=JSON_DECODE_ERROR_BACKOFF_2, error={str(je_r2)}, content_snip={candidate[:200]}")
                                        except Exception:
                                            pass
                                else:
                                    try:
                                        snip = content_retry[:200] if isinstance(content_retry, str) else str(type(content_retry))
                                        log_event("LLM_PARSE_FALLBACK", f"reason=JSON_DECODE_ERROR_BACKOFF, content_snip={snip}")
                                    except Exception:
                                        pass
                            if isinstance(parsed_retry, dict):
                                actions_retry = parsed_retry.get("actions", [])
                                if isinstance(actions_retry, list) and actions_retry:
                                    self.last_parse_mode = "llm"
                                    try:
                                        log_event("LLM_PARSE_MODE", self.last_parse_mode)
                                    except Exception:
                                        pass
                                    return actions_retry
                        time.sleep(delay)
                    try:
                        log_event("LLM_PARSE_FALLBACK", f"reason=HTTP_5XX_RETRY_EXHAUSTED, status={status}")
                    except Exception:
                        pass
            except requests.exceptions.RequestException as rexc:
                try:
                    log_event("LLM_PARSE_FALLBACK", f"reason=RequestException, detail={str(rexc)}")
                except Exception:
                    pass
                # 快速重试一次：连接/请求超时
                try:
                    is_timeout = isinstance(rexc, requests.exceptions.Timeout) or isinstance(rexc, requests.exceptions.ConnectTimeout) or ("ConnectTimeout" in str(rexc)) or ("ConnectTimeoutError" in str(rexc))
                except Exception:
                    is_timeout = "ConnectTimeout" in str(rexc)
                if is_timeout:
                    try:
                        log_event("LLM_PARSE_RETRY", "reason=QUICK_RETRY_CONNECT_TIMEOUT, next_timeout=8s")
                    except Exception:
                        pass
                    try:
                        content_fast = self.chat(
                            messages=[
                                {"role": "system", "content": "你是严谨的中文助手，只输出有效JSON。"},
                                {"role": "user", "content": prompt},
                            ],
                            response_json=False,
                            timeout=8,
                        )
                    except requests.exceptions.RequestException as rexc_fast:
                        try:
                            log_event("LLM_PARSE_FALLBACK", f"reason=RequestException_FAST_RETRY, detail={str(rexc_fast)}")
                        except Exception:
                            pass
                    except Exception as e_fast:
                        try:
                            log_event("LLM_PARSE_FALLBACK", f"reason=LLM_CHAT_EXCEPTION_FAST_RETRY, detail={str(e_fast)}, stack={traceback.format_exc()[:600]}")
                        except Exception:
                            pass
                    else:
                        # 解析快速重试返回
                        parsed_fast = None
                        try:
                            parsed_fast = json.loads(content_fast)
                        except Exception:
                            candidate = self._extract_json_text(content_fast)
                            if candidate:
                                try:
                                    parsed_fast = json.loads(candidate)
                                except Exception as je_fast:
                                    try:
                                        log_event("LLM_PARSE_FALLBACK", f"reason=JSON_DECODE_ERROR_FAST_RETRY_2, error={str(je_fast)}, content_snip={candidate[:200]}")
                                    except Exception:
                                        pass
                            else:
                                try:
                                    snip = content_fast[:200] if isinstance(content_fast, str) else str(type(content_fast))
                                    log_event("LLM_PARSE_FALLBACK", f"reason=JSON_DECODE_ERROR_FAST_RETRY, content_snip={snip}")
                                except Exception:
                                    pass
                        if isinstance(parsed_fast, dict):
                            actions_fast = parsed_fast.get("actions", [])
                            if isinstance(actions_fast, list) and actions_fast:
                                self.last_parse_mode = "llm"
                                try:
                                    log_event("LLM_PARSE_MODE", self.last_parse_mode)
                                except Exception:
                                    pass
                                return actions_fast
                            else:
                                try:
                                    detail = f"type={type(actions_fast).__name__}, len={len(actions_fast) if isinstance(actions_fast, list) else 'N/A'}"
                                    log_event("LLM_PARSE_FALLBACK", f"reason=INVALID_OR_EMPTY_ACTIONS_FAST_RETRY, {detail}")
                                except Exception:
                                    pass
            except Exception as e:
                try:
                    log_event("LLM_PARSE_FALLBACK", f"reason=LLM_CHAT_EXCEPTION, detail={str(e)}, stack={traceback.format_exc()[:600]}")
                except Exception:
                    pass
            else:
                # 2) 解析LLM返回的JSON
                parsed = None
                try:
                    parsed = json.loads(content)
                except Exception as je:
                    snip = content[:200] if isinstance(content, str) else str(type(content))
                    try:
                        log_event("LLM_PARSE_FALLBACK", f"reason=JSON_DECODE_ERROR, content_snip={snip}, error={str(je)}")
                    except Exception:
                        pass
                    
                    # 尝试从自然语言中提取JSON
                    candidate = self._extract_json_text(content)
                    if candidate:
                        try:
                            parsed = json.loads(candidate)
                            try:
                                log_event("LLM_PARSE_RETRY", f"reason=JSON_EXTRACTED_FROM_TEXT, candidate_snip={candidate[:200]}")
                            except Exception:
                                pass
                        except Exception as je2:
                            try:
                                log_event("LLM_PARSE_FALLBACK", f"reason=JSON_DECODE_ERROR_EXTRACTED, error={str(je2)}, candidate_snip={candidate[:200]}")
                            except Exception:
                                pass
                    
                    # 如果提取JSON也失败，尝试降级重试（不带response_format）
                    if parsed is None:
                        try:
                            log_event("LLM_PARSE_RETRY", "reason=FALLBACK_NO_RESPONSE_FORMAT")
                        except Exception:
                            pass
                        try:
                            content_fallback = self.chat(
                                messages=[
                                    {"role": "system", "content": "你是严谨的中文助手，只输出有效JSON格式的动作列表。"},
                                    {"role": "user", "content": prompt},
                                ],
                                response_json=False,
                            )
                            # 记录降级响应内容
                            try:
                                fallback_snip = (content_fallback[:300] if isinstance(content_fallback, str) else str(type(content_fallback)))
                                log_event("LLM_PARSE_RESPONSE", f"fallback_content_snip={fallback_snip}")
                            except Exception:
                                pass
                            
                            # 尝试解析降级响应
                            try:
                                parsed = json.loads(content_fallback)
                            except Exception:
                                candidate_fallback = self._extract_json_text(content_fallback)
                                if candidate_fallback:
                                    try:
                                        parsed = json.loads(candidate_fallback)
                                        try:
                                            log_event("LLM_PARSE_RETRY", f"reason=JSON_EXTRACTED_FROM_FALLBACK, candidate_snip={candidate_fallback[:200]}")
                                        except Exception:
                                            pass
                                    except Exception as je3:
                                        try:
                                            log_event("LLM_PARSE_FALLBACK", f"reason=JSON_DECODE_ERROR_FALLBACK, error={str(je3)}, candidate_snip={candidate_fallback[:200]}")
                                        except Exception:
                                            pass
                        except Exception as e_fallback:
                            try:
                                log_event("LLM_PARSE_FALLBACK", f"reason=FALLBACK_REQUEST_EXCEPTION, detail={str(e_fallback)}")
                            except Exception:
                                pass
                
                if isinstance(parsed, dict):
                    actions = parsed.get("actions", [])
                    if isinstance(actions, list) and actions:
                        self.last_parse_mode = "llm"
                        try:
                            log_event("LLM_PARSE_MODE", self.last_parse_mode)
                        except Exception:
                            pass
                        return actions
                    else:
                        detail = f"type={type(actions).__name__}, len={len(actions) if isinstance(actions, list) else 'N/A'}"
                        try:
                            log_event("LLM_PARSE_FALLBACK", f"reason=INVALID_OR_EMPTY_ACTIONS, {detail}")
                        except Exception:
                            pass
            # 若上述任一步失败，继续回退到规则解析
        else:
            # 不可用时明确记录跳过原因
            try:
                log_event(
                    "LLM_PARSE_SKIP",
                    f"available=false, api_key={'set' if self.api_key else 'missing'}, api_url={'set' if self.api_url else 'missing'}"
                )
            except Exception:
                pass

        rb_actions = self._rule_based_parse(text)
        self.last_parse_mode = "rule"
        try:
            log_event("LLM_PARSE_MODE", self.last_parse_mode)
        except Exception:
            pass
        return rb_actions

    def parse_with_tool_hints(self, text: str, tool_hints: List[str]) -> List[Dict[str, Any]]:
        """Parse command with explicit tool hints injected into the prompt.

        Returns list of actions in the same format as parse_command.
        """
        if not self.available():
            # Fallback to existing parser when LLM is not configured
            return self.parse_command(text)

        hint_lines = "\n".join(tool_hints or [])
        prompt = (
            "你是一个指令解析器。请将用户意图解析成可执行的动作列表。只输出JSON，不要解释。\n"
            "仅使用以下动作工具：\n" + hint_lines + "\n\n"
            "输出格式示例：{\"actions\":[{\"type\":\"web_search\",\"query\":\"周星驰\"}]}\n\n"
            f"用户指令：{text}"
        )
        try:
            content = self.chat(
                messages=[
                    {"role": "system", "content": "你是严谨的中文助手，只输出有效JSON。"},
                    {"role": "user", "content": prompt},
                ],
                response_json=True,
            )
            if isinstance(content, str):
                try:
                    parsed = json.loads(content)
                    actions = parsed.get("actions", [])
                    if isinstance(actions, list):
                        return actions
                except Exception:
                    pass
            # Structured output not available, extract first JSON object
            try:
                jt = self._extract_json_text(content)
                if jt:
                    parsed = json.loads(jt)
                    actions = parsed.get("actions", [])
                    if isinstance(actions, list):
                        return actions
            except Exception:
                pass
        except Exception:
            pass
        # Fallback
        return self.parse_command(text)

    def _rule_based_parse(self, text: str) -> List[Dict[str, Any]]:
        text = text.strip()
        actions: List[Dict[str, Any]] = []

        # Play music heuristics
        if any(k in text for k in ["播放", "放歌", "来一首"]):
            # Try to extract song name within quotes or after 播放
            m = re.search(r"播放[《\"']?([^《\"']+)[》\"']?", text)
            song_name = m.group(1).strip() if m else text.replace("播放", "").strip()
            if song_name:
                actions.append({"type": "play_music", "song_name": song_name})

        # Write article heuristics
        if "写" in text and ("文章" in text or "作文" in text or "文" in text):
            # words
            wm = re.search(r"(\d+)(?:字|词|字数)", text)
            words = int(wm.group(1)) if wm else 300
            # topic
            tm = re.search(r"关于([^，。,【,\n]+)", text)
            topic = tm.group(1).strip() if tm else text
            # save path: detect explicit absolute path or E盘关键词
            sp_match = re.search(r"保存到\s*([A-Za-z]:[^，。\n]*)", text)
            if sp_match:
                save_path = sp_match.group(1).strip()
            elif ("E盘" in text) or ("E 盘" in text) or ("E盘下" in text):
                save_path = "E:/"
            elif ("桌面" in text):
                save_path = "Desktop"
            elif ("我的文档" in text or "文档" in text):
                save_path = "Documents"
            else:
                save_path = "Default"
            actions.append({"type": "write_article", "topic": topic, "words": words, "save_path": save_path})

        # File operations heuristics
        # Create file: 在<目录>创建(一个)?名为<name>的文件 / 创建文件 <path>
        cm = re.search(r"在(.+?)创建(?:一个)?(?:名为|叫)?(.+?)的文件", text)
        pm = re.search(r"创建文件\s+([^，。\n]+)", text)
        if cm:
            dir_hint = cm.group(1).strip()
            fname = cm.group(2).strip()
            actions.append({"type": "file_op", "op": "create", "name": fname, "src": dir_hint})
        elif pm:
            path_hint = pm.group(1).strip()
            actions.append({"type": "file_op", "op": "create", "src": path_hint})

        # Delete file: 删除 <path/name>
        dm = re.search(r"删除\s+([^，。\n]+)", text)
        if dm:
            target = dm.group(1).strip()
            actions.append({"type": "file_op", "op": "delete", "src": target})

        # Move file: 把 <src> 移动到 <dest> / 移动文件 从<src>到<dest>
        mm = re.search(r"把\s+([^，。\n]+?)\s+移动到\s+([^，。\n]+)", text)
        m2 = re.search(r"移动文件\s+从\s*([^，。\n]+)\s*到\s*([^，。\n]+)", text)
        if mm:
            actions.append({"type": "file_op", "op": "move", "src": mm.group(1).strip(), "dest": mm.group(2).strip()})
        elif m2:
            actions.append({"type": "file_op", "op": "move", "src": m2.group(1).strip(), "dest": m2.group(2).strip()})

        # Copy file: 复制/拷贝 <src> 到 <dest>
        cpm = re.search(r"(?:复制|拷贝)\s+([^，。\n]+?)\s+到\s+([^，。\n]+)", text)
        if cpm:
            actions.append({"type": "file_op", "op": "copy", "src": cpm.group(1).strip(), "dest": cpm.group(2).strip()})

        # Rename file: 重命名 <src> 为 <dest>
        rnm = re.search(r"重命名\s+([^，。\n]+?)\s+为\s+([^，。\n]+)", text)
        if rnm:
            actions.append({"type": "file_op", "op": "rename", "src": rnm.group(1).strip(), "dest": rnm.group(2).strip()})

        # List dir: 列出/查看 <目录>
        lsm = re.search(r"(?:列出|查看)\s+([^，。\n]+)", text)
        if lsm:
            actions.append({"type": "file_op", "op": "list", "src": lsm.group(1).strip()})

        # Open file: 打开 <path>
        ofm = re.search(r"打开\s+([^，。\n]+)\.(?:txt|docx|pdf|png|jpg|wav|mp3|mp4)", text)
        if ofm:
            actions.append({"type": "file_op", "op": "open", "src": ofm.group(0).replace("打开", "").strip()})

        # Web automation heuristics
        # Web search
        if any(k in text for k in ["搜索", "查找", "检索"]):
            sqm = re.search(r"(?:搜索|查找|检索)\s*[《\"']?([^《\"'》]+)[》\"']?", text)
            if sqm:
                query = sqm.group(1).strip()
                if query:
                    actions.append({"type": "web_search", "query": query})
        # Navigate (URL explicitly or domain)
        urlm = re.search(r"(https?://[^\s，。]+)", text)
        if urlm:
            actions.append({"type": "navigate", "url": urlm.group(1)})
        else:
            domm = re.search(r"(?:导航到|打开)\s*([A-Za-z0-9.-]+\.[A-Za-z]{2,})(/[^\s，。]*)?", text)
            if domm:
                url = domm.group(1) + (domm.group(2) or "")
                if not url.startswith("http"):
                    url = "https://" + url
                actions.append({"type": "navigate", "url": url})
        # Browser click (first result or by link text)
        fc = re.search(r"点击(?:搜索结果中的)?(?:第(?:一|1)(?:条|个|项)|首条|首个|首项)", text)
        if fc:
            actions.append({"type": "browser_click", "target": "first_result"})
        else:
            clm = re.search(r"点击(?:链接|结果)?\s*[《\"']?([^《\"'》]+)[》\"']?", text)
            if clm:
                link_text = clm.group(1).strip()
                if link_text:
                    actions.append({"type": "browser_click", "target": "link_text", "value": link_text})

        # App control: 打开/启动/关闭/退出 <应用>
        ap_open = re.search(r"(?:打开|启动)\s*([^，。\n]+)", text)
        ap_close = re.search(r"(?:关闭|退出)\s*([^，。\n]+)", text)

        # 仅选择一个目标应用，避免将长串作为 app 传入执行层
        def _pick_single_app(segment: str) -> str:
            tokens = re.split(r"[，、,；;\s]+", segment)
            tokens = [t.strip() for t in tokens if t.strip()]
            # 优先：直接命中别名键
            for tkn in tokens:
                if tkn in Config.APP_ALIASES:
                    return tkn
            # 其次：对第一个词做近似匹配
            try:
                from difflib import get_close_matches
                base = tokens[0] if tokens else segment
                matches = get_close_matches(base, list(Config.APP_ALIASES.keys()), n=1, cutoff=0.6)
                if matches:
                    return matches[0]
            except Exception:
                pass
            # 兜底：返回第一个非空词
            return tokens[0] if tokens else segment

        if ap_open:
            app_sel = _pick_single_app(ap_open.group(1).strip())
            actions.append({"type": "app_control", "op": "open", "app": app_sel})
        if ap_close:
            app_sel = _pick_single_app(ap_close.group(1).strip())
            actions.append({"type": "app_control", "op": "close", "app": app_sel})
        # 新增：显式登录微信/进入微信
        if re.search(r"(登录|登陆|进入)\s*微信", text):
            actions.append({"type": "app_control", "op": "login", "app": "微信"})
        if "微信" in text and re.search(r"发送", text):
            # 联系人：优先匹配“找到/给 <联系人> (这个)?聊天窗口”
            contact = None
            m1 = re.search(r"(?:找到|给)\s*([^，。\n]+?)(?:这个)?(?:聊天窗口)?", text)
            if m1:
                contact = m1.group(1).strip()
            else:
                # 备选：直接出现“文件传输助手”或其他词
                m2 = re.search(r"(文件传输助手)", text)
                if m2:
                    contact = m2.group(1).strip()
            msg = None
            m3 = re.search(r"发送\s*([^，。\n]+)", text)
            if m3:
                msg = m3.group(1).strip()
            if contact and msg:
                actions.append({"type": "wechat_message", "contact": contact, "text": msg})
        # 新增：QQ 内发送消息（找到联系人并发送）
        if "QQ" in text and re.search(r"发送", text):
            contact = None
            m1 = re.search(r"(?:找到|给)\s*([^，。\n]+?)(?:这个)?(?:聊天窗口)?", text)
            if m1:
                contact = m1.group(1).strip()
            else:
                m2 = re.search(r"(我的手机|文件传输助手)", text)
                if m2:
                    contact = m2.group(1).strip()
            msg = None
            m3 = re.search(r"发送\s*([^，。\n]+)", text)
            if m3:
                msg = m3.group(1).strip()
            if contact and msg:
                actions.append({"type": "qq_message", "contact": contact, "text": msg})
        # System control: 音量/亮度
        # 音量设置为XX%/调高音量/调低音量/静音/取消静音
        vol_set = re.search(r"音量(?:设置为|调到)\s*(\d{1,3})%", text)
        if vol_set:
            actions.append({"type": "system_control", "op": "set_volume", "value": int(vol_set.group(1))})
        if "静音" in text:
            actions.append({"type": "system_control", "op": "mute"})
        if "取消静音" in text or "恢复声音" in text:
            actions.append({"type": "system_control", "op": "unmute"})
        if "调高音量" in text or "加大音量" in text:
            actions.append({"type": "system_control", "op": "increase_volume", "value": 10})
        if "调低音量" in text or "降低音量" in text:
            actions.append({"type": "system_control", "op": "decrease_volume", "value": 10})

        # 亮度设置为XX%/调亮屏幕/调暗屏幕
        bri_set = re.search(r"亮度(?:设置为|调到)\s*(\d{1,3})%", text)
        if bri_set:
            actions.append({"type": "system_control", "op": "set_brightness", "value": int(bri_set.group(1))})
        if "调亮屏幕" in text or "提高亮度" in text:
            actions.append({"type": "system_control", "op": "increase_brightness", "value": 10})
        if "调暗屏幕" in text or "降低亮度" in text:
            actions.append({"type": "system_control", "op": "decrease_brightness", "value": 10})

        if not actions:
            clar_qs: List[str] = []
            if any(k in text for k in ["播放", "放歌", "来一首"]) and not re.search(r"播放[《\"']?([^《\"']+)[》\"']?", text):
                clar_qs.append("要播放哪首歌曲？")
            if ("写" in text and ("文章" in text or "作文" in text or "文" in text)) and not re.search(r"关于([^，。,【,\n]+)", text):
                clar_qs.append("文章主题与字数是多少？保存到哪里？")
            if any(k in text for k in ["搜索", "查找", "检索"]) and not re.search(r"(?:搜索|查找|检索)\s*[《\"']?([^《\"'》]+)[》\"']?", text):
                clar_qs.append("要搜索的关键词是什么？是否点击第一条结果？")
            if re.search(r"(?:打开|启动)\s*$", text):
                clar_qs.append("要打开的应用名称是什么？")
            question = "；".join(clar_qs) if clar_qs else "请问具体要播放哪首音乐、写什么主题，或要进行的文件/应用/系统操作？"
            actions.append({"type": "needs_clarification", "question": question})
        return actions

    def generate_article_content(self, topic: str, words: int = 300) -> str:
        """Generate article content using LLM, fallback to a local template."""
        if self.available():
            prompt = (
                f"请写一篇关于{topic}的中文文章，目标字数约为{words}字。避免使用分点列表，保持自然流畅。"
            )
            try:
                content = self.chat(
                    messages=[
                        {"role": "system", "content": "你是中文写作助手。"},
                        {"role": "user", "content": prompt},
                    ],
                    response_json=False,
                )
                if content:
                    self.last_generate_mode = "llm"
                    try:
                        log_event("LLM_GENERATE_MODE", self.last_generate_mode)
                    except Exception:
                        pass
                    return content.strip()
            except Exception:
                pass
        # Fallback content
        self.last_generate_mode = "template"
        try:
            log_event("LLM_GENERATE_MODE", self.last_generate_mode)
        except Exception:
            pass
        return self._fallback_article(topic, words)

    def _fallback_article(self, topic: str, words: int) -> str:
        para = (
            f"{topic}这一主题，既蕴含深厚的历史背景，也连接着现实生活的脉络。"
            f"我们从日常经验与公共认知出发，尝试在有限篇幅里呈现其逻辑与温度。"
            f"在具体讨论中，既要关注细节变化，也要把握整体趋势，避免偏见与片面。"
            f"最后，希望读者能够在思考与实践中，对{topic}形成更清晰而开放的理解。"
        )
        repeats = max(1, words // 80)
        return (para + "\n") * repeats